﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace OpenPop.Mime.Decode
{
	/// <summary>
	/// Utility class used by OpenPop for mapping from a characterSet to an <see cref="Encoding"/>.<br/>
	/// <br/>
	/// The functionality of the class can be altered by adding mappings
	/// using <see cref="AddMapping"/> and by adding a <see cref="FallbackDecoder"/>.<br/>
	/// <br/>
	/// Given a characterSet, it will try to find the Encoding as follows:
	/// <list type="number">
	///     <item>
	///         <description>If a mapping for the characterSet was added, use the specified Encoding from there. Mappings can be added using <see cref="AddMapping"/>.</description>
	///     </item>
	///     <item>
	///         <description>Try to parse the characterSet and look it up using <see cref="Encoding.GetEncoding(int)"/> for codepages or <see cref="Encoding.GetEncoding(string)"/> for named encodings.</description>
	///     </item>
	///     <item>
	///         <description>If an encoding is not found yet, use the <see cref="FallbackDecoder"/> if defined. The <see cref="FallbackDecoder"/> is user defined.</description>
	///     </item>
	/// </list>
	/// </summary>
	public static class EncodingFinder
	{
		/// <summary>
		/// Delegate that is used when the EncodingFinder is unable to find an encoding by
		/// using the <see cref="EncodingFinder.EncodingMap"/> or general code.<br/>
		/// This is used as a last resort and can be used for setting a default encoding or
		/// for finding an encoding on runtime for some <paramref name="characterSet"/>.
		/// </summary>
		/// <param name="characterSet">The character set to find an encoding for.</param>
		/// <returns>An encoding for the <paramref name="characterSet"/> or <see langword="null"/> if none could be found.</returns>
		public delegate Encoding FallbackDecoderDelegate(string characterSet);

		/// <summary>
		/// Last resort decoder.
		/// </summary>
		public static FallbackDecoderDelegate FallbackDecoder { private get; set; }

		/// <summary>
		/// Mapping from charactersets to encodings.
		/// </summary>
		private static Dictionary<string, Encoding> EncodingMap { get; set; }

		/// <summary>
		/// Initialize the EncodingFinder
		/// </summary>
		static EncodingFinder()
		{
			Reset();
		}

		/// <summary>
		/// Used to reset this static class to facilite isolated unit testing.
		/// </summary>
		internal static void Reset()
		{
			EncodingMap = new Dictionary<string, Encoding>();
			FallbackDecoder = null;

			// Some emails incorrectly specify the encoding as utf8, but it should have been utf-8.
			AddMapping("utf8", Encoding.UTF8);
		}

		/// <summary>
		/// Parses a character set into an encoding.
		/// </summary>
		/// <param name="characterSet">The character set to parse</param>
		/// <returns>An encoding which corresponds to the character set</returns>
		/// <exception cref="ArgumentNullException">If <paramref name="characterSet"/> is <see langword="null"/></exception>
		internal static Encoding FindEncoding(string characterSet)
		{
			if (characterSet == null)
				throw new ArgumentNullException("characterSet");

			string charSetUpper = characterSet.ToUpperInvariant();

			// Check if the characterSet is explicitly mapped to an encoding
			if (EncodingMap.ContainsKey(charSetUpper))
				return EncodingMap[charSetUpper];

			// Try to generally find the encoding
			try
			{
				if (charSetUpper.Contains("WINDOWS") || charSetUpper.Contains("CP"))
				{
					// It seems the characterSet contains an codepage value, which we should use to parse the encoding
					charSetUpper = charSetUpper.Replace("CP", ""); // Remove cp
					charSetUpper = charSetUpper.Replace("WINDOWS", ""); // Remove windows
					charSetUpper = charSetUpper.Replace("-", ""); // Remove - which could be used as cp-1554

					// Now we hope the only thing left in the characterSet is numbers.
					int codepageNumber = int.Parse(charSetUpper, CultureInfo.InvariantCulture);

					return Encoding.GetEncoding(codepageNumber);
				}

				// It seems there is no codepage value in the characterSet. It must be a named encoding
				return Encoding.GetEncoding(characterSet);
			}
			catch (ArgumentException)
			{
				// The encoding could not be found generally. 
				// Try to use the FallbackDecoder if it is defined.

				// Check if it is defined
				if (FallbackDecoder == null)
					throw; // It was not defined - throw catched exception

				// Use the FallbackDecoder
				Encoding fallbackDecoderResult = FallbackDecoder(characterSet);

				// Check if the FallbackDecoder had a solution
				if (fallbackDecoderResult != null)
					return fallbackDecoderResult;

				// If no solution was found, throw catched exception
				throw;
			}
		}

		/// <summary>
		/// Puts a mapping from <paramref name="characterSet"/> to <paramref name="encoding"/>
		/// into the <see cref="EncodingFinder"/>'s internal mapping Dictionary.
		/// </summary>
		/// <param name="characterSet">The string that maps to the <paramref name="encoding"/></param>
		/// <param name="encoding">The <see cref="Encoding"/> that should be mapped from <paramref name="characterSet"/></param>
		/// <exception cref="ArgumentNullException">If <paramref name="characterSet"/> is <see langword="null"/></exception>
		/// <exception cref="ArgumentNullException">If <paramref name="encoding"/> is <see langword="null"/></exception>
		public static void AddMapping(string characterSet, Encoding encoding)
		{
			if (characterSet == null)
				throw new ArgumentNullException("characterSet");

			if(encoding == null)
				throw new ArgumentNullException("encoding");

			// Add the mapping using uppercase
			EncodingMap.Add(characterSet.ToUpperInvariant(), encoding);
		}
	}
}
